iT邦幫忙

2026 iThome 鐵人賽

DAY 22
0
Software Development

打造 OS Kernel:從 OS in 1,000 Lines 到 xv6系列 第 22 篇

Day 22|swtch.S:把 Tiny OS 與 xv6 Context Switch 放在一起比較

  • 分享至 

  • xImage
  •  

CPU 在 swtch() 中執行 ret,下一個位置卻可能是另一份堆疊上的程式。
這個現象在 Day 11 已經出現過,今天換成 xv6,並把中間的 scheduler context 看清楚。

最重要的問題不是這次保存幾個暫存器,而是:誰保存誰,誰又負責讓它恢復?

兩次切換,不是 P1 直接指定 P2

xv6 的正常路徑可以寫成:

P1 的核心執行 → 這個 CPU 的 scheduler context
                         → 被選中的 P2 核心執行

scheduler() 呼叫 swtch(&c->context, &p->context),將 CPU 的排程器現場存好,再恢復行程現場。
反方向則由 sched() 呼叫 swtch(&p->context, &mycpu()->context)。

這裡的 sched() 與 scheduler() 名字相近,責任不同。
前者是行程切回排程器的交接點,後者才包含挑選 RUNNABLE 行程的迴圈。

RV64 改變了欄位寬度

在 30-days-os-kernel/examples/xv6-riscv/ 閱讀 kernel/swtch.S,再與前面範例的 switch.S 對照:

項目 Tiny Kernel RV32 xv6 RV64
保存指令 sw sd
恢復指令 lw ld
ra offset 0 0
sp offset 4 8
s0 offset 8 16

保存對象仍是 ra、sp 與 callee-saved registers,沒有因此變成完整 Trap Frame。
進入 swtch() 的前提是正常函式呼叫邊界,caller-saved registers 的責任仍由呼叫慣例處理。
計時器在任意位置打斷程式時,還有另外一層 Trap 保存機制。

在切換邊界停住

沿用 Day 20 的單 hart QEMU 除錯參數,包含 -S -gdb tcp:127.0.0.1:1234。
另一個終端機在同一 xv6 checkout 執行:

gdb-multiarch -nx kernel/kernel

GDB 指令:

target remote 127.0.0.1:1234
hbreak *swtch
continue
set $oldctx = (struct context *)$a0
set $newctx = (struct context *)$a1
p *$oldctx
p *$newctx
info registers ra sp s0 s1
x/30i $pc

目前 oldctx 是即將被覆寫的保存區,不一定已經代表剛才那份執行現場。
要比較保存結果,需先用 si 執行保存指令,再重新讀取它。
同樣地,恢復指令完成後的 sp 與 ra 應對應 newctx,而不是維持進入函式時的值。

第一次被排程的行程沒有「上次暫停位置」,它的 context.ra 是 allocproc() 預先設定的 forkret。
這與 Tiny Kernel 的 process_start() 入口包裝相呼應,但 xv6 的 forkret() 還要銜接檔案系統初始化與使用者返回路徑。

加一個有上限的切換紀錄

為了不讓每次切換都大量輸出,在 kernel/proc.h 的 struct proc 加入:

uint64 scheduled; // protected by p->lock

在 allocproc() 的 p->state = USED; 後設定 p->scheduled = 0;。
接著在 scheduler() 的 p->state = RUNNING; 後、呼叫 swtch() 前加入:

p->scheduled++;
if (p->scheduled <= 2)
  printk("dispatch cpu=%d pid=%d n=%lu\n",
         cpuid(), p->pid, p->scheduled);

計數器在持有 p->lock 的區間更新,每個新行程只印前兩次。
這份計數從今天開始保留,Day 23 直接沿用,不要再插入第二個遞增位置。

以一般 make TOOLPREFIX=riscv64-linux-gnu- CPUS=1 qemu 啟動,觀察 dispatch 訊息。
scheduled 是 CPU 交給行程的次數,不是工作量,也不等於 timer interrupt 次數。

為什麼 p->lock 會跨過切換?

xv6 在把行程從 RUNNABLE 改成 RUNNING、設定 CPU 所屬行程與切換堆疊時,必須維持共同的不變條件。
如果太早釋放鎖,另一個 CPU 可能把尚未完成交接的行程當成可執行工作。

這是一個特殊但刻意的交接協定:鎖可以由切換另一側的程式碼釋放。
不要在 swtch() 前擅自補上 release(&p->lock),也不要讓其他不相關的鎖一起跨過 sched()。
sched() 中的 holding()、noff 與中斷狀態檢查,就是幫我們抓出違反協定的位置。

本日修改 proc.h 與 proc.c,並保存自己的 GDB 觀察紀錄,建議 commit:

day22: trace xv6 context-switch boundaries

下一篇開始用工作負載觀察這個計數器,而不是只看一段切換組語。

參考資料


上一篇
Day 21|恐龍書的 PCB 在 xv6 裡長什麼樣?深入 struct proc
系列文
打造 OS Kernel:從 OS in 1,000 Lines 到 xv6 共 22 篇
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言